WebRTC源码分析

您所在的位置:网站首页 channel type翻译 WebRTC源码分析

WebRTC源码分析

2023-10-11 07:04| 来源: 网络整理| 查看: 265

目录 1. 引言2. DataChannelController2.1 DataChannelController继承结构2.2 数据和通道状态上行2.3 数据下行2.4 DataChannelController什么时候创建的? 3. DataChannelTransportInterface3.1 DataChannelTransportInterface创建时机追踪3.1.1 PeerConnection::SetupDataChannelTransport_n3.1.2 PeerConnection::CreateDataChannel3.1.3 PeerConnection::UpdateDataChannel3.1.4 PeerConnection::UpdateTransceiversAndDataChannels3.1.5 PeerConnection::ApplyRemoteDescription3.1.6 PeerConnection::PushdownTransportDescription3.1.6 JsepTransportController::SetLocalDescription3.1.7 JsepTransportController::ApplyDescription_n3.1.8 JsepTransportController::MaybeCreateJsepTransport3.1.8 JsepTransport::JsepTransport3.1.9 JsepTransport::data_channel_transport 3.2 作图以做总结 4. DataChannel4.1 DataChannel的成员变量4.2 DataChannel的状态流转4.3 DataChannel数据发送、接收、缓存 、统计4.3.1 用户主动发送数据4.3.2 底层Transport由block变为可写——>发送排队数据4.3.3 接收数据4.3.3 数据的缓存 5. 总结

1. 引言

我们在文章 WebRTC源码分析-呼叫建立过程之四(下)(创建数据通道DataChannel) 中分析了DataChannel的创建过程,但是也遗留了一些问题留待需要解答:

DataChannelController这个对象是什么时候创建的呢?SCTP底层传输对象DataChannelTransportInterface到底实体类是哪个?什么时候创建的?DataChannelController与DataChannelTransportInterface是如何建立关联,又是在何时建立的关联?

本文将在分析DataChannel相关的几个类的基础上来一一解答上述问题。另外由于SCTP是DataChannel主流的底层传输方式,RTP类别的底层传输是未来要弃用的,因此,本文将只针对SCTP进行分析。

2. DataChannelController

DataChannelController类是WebRTC数据通道的聚合器,保存着所有数据通道的上层对象DataChannel,也保存着数据通道的底层传输——实现DataChannelTransportInterface接口的对象(RTP协议是RtpDataChannel,后续将略过不再提)。同时,它起着桥梁作用,将上层DataChannel要发送的数据,传递给底层通道DataChannelTransportInterface进行网络数据包发送,同时又将底层通道的状态以及接收到的数据回传给上层DataChannel。在这里插入图片描述 PC、DataChannelController、DataChannel、DataChannelTransport的关系如上图所示。如果,在更新的代码中DataChannelController不存在了也不用惊讶,它所有的功能都迁移到PC中了。

2.1 DataChannelController继承结构

在这里插入图片描述 上图是DataChannelController的继承结构图,对于DataChannelController类我省略了如下几个部分的东西:

RTP相关的成员与方法成员的getter/setter方法对接口DataChannelSink && DataChannelProviderInterface的方法实现 2.2 数据和通道状态上行

DataChannelTransportInterface——>DataChannelController: DataChannelController实现接口DataChannelSink,通过data_channel_transport_.SetDataSink方法将自已以Sink概念注入到底层的传输通道对象中,以此实现DataChannelController从data_channel_transport_获取对端传来的数据,并观察底层传输通道状态。假如data_channel_transport_的实体类是SctpDataChannelTransport,那么数据的向DataChannelController传递如下图所示: 在这里插入图片描述 另DataChannelController实现DataChannelSink接口时,以实现数据接收方法OnDataReceived为例:稍微做了下参数转换后,然后异步方式投递DataChannelController的信号SignalDataChannelTransportReceivedData_s。注意:OnDataReceived是提供给底层的回调,底层的收发包是在网络线程中进行的,因此,OnDataReceived也是在network线程中执行。而更上层的数据处理必须代理到信令线程执行 在这里插入图片描述

DataChannelController——>DataChannel: 在文章 WebRTC源码分析-呼叫建立过程之四(下)(创建数据通道DataChannel) 中,我们已经知道创建DataChannel时,DataChannel会关联DataChannelController的信号:由此,数据到达信号SignalDataChannelTransportReceivedData_s会触发DataChannel::OnDataReceived方法,完成数据传导到DataChannel。 在这里插入图片描述

DataChannel——>应用层: DataChannel中会保存它的观察者对象成员DataChannelObserver* observer_,当数据到达时,通过调用observer_->OnMessage()方法传导数据到应用层。 在这里插入图片描述

2.3 数据下行

应用层——>DataChannel: DataChannel向底层发送数据有两大类:一类是应用数据,具体还分为二进制数据和文本数据;一类是控制信令,用于SCTP的握手状态协商。

发送应用数据的时机 当应用层调用DataChannel::Send方法时,将向投底层投递应用数据,当底层未处于可发送状态时,数据会在DataChannel的PacketQueue queued_send_data_成员中进行排队,如下图调用QueueSendDataMessage方法。底层处于可发送状态时,将向底层发送数据,如下图调用SendDataMessage方法。 在这里插入图片描述

发送控制信令的时机 DataChannel保存着该层状态DataState,新创建的DataChannel处于kConnecting状态,处于该状态时,DataChannel不能发送应用数据,只能发送握手控制信令。 在这里插入图片描述 当底层通道准备好时(如上分析,状态会上行到DataChannel),调用DataChannel的UpdateState方法 ,通过判断当时的DataChannel状态值 && 握手角色来构造合适的握手控制信令,并向底层投递,如下图调用SendControlMessage()方法 在这里插入图片描述

DataChannel——>DataChannelController: 当构造DataChannel时,我们知道DataChannelController会被存储到DataChannel中,作为provider存在。而DataChannelController通过实现DataChannelProviderInterface接口,提供了发送数据的方法SendData()。DataChannel不论是调用SendDataMessage()方法发送应用层数据也好,还是调用SendControlMessage()发送握手控制信令也好,最终都是通过调用provider->SendData()完成数据下发,从而数据传到到DataChannelController。 在这里插入图片描述

DataChannelController——>DataChannelTransportInterface: DataChannelController的SendData方法中会同步调用DataChannelTransportInterface的SendData方法,该方法是在network线程中执行,并返回结果 在这里插入图片描述

PS: 通过上述分析,底层传输通道工作在network线程中,上层DataChannel层工作在信令线程中,DataChannelController在数据/状态传递过程中还需要进行线程之间的转换。向上传递时,通过成员std::unique_ptr data_channel_transport_invoker_的AsyncInvoker方法进行转换;向下转换时,通过网络线程的Invoke方法同步调用底层通道的方法。

2.4 DataChannelController什么时候创建的?

现在,回答引言中的第一个问题:DataChannelController什么时候创建的?由于DataChannelController作为PC的成员变量,不是以指针形式存在,因此,在创建PC时,DataChannelController就已经被创建 在这里插入图片描述

3. DataChannelTransportInterface

DataChannelTransportInterface接口代表了数据通道的底层传输,其声明如下:

class DataChannelTransportInterface { public: virtual ~DataChannelTransportInterface() = default; // Opens a data |channel_id| for sending. May return an error if the // specified |channel_id| is unusable. Must be called before |SendData|. virtual RTCError OpenChannel(int channel_id) = 0; // Sends a data buffer to the remote endpoint using the given send parameters. // |buffer| may not be larger than 256 KiB. Returns an error if the send // fails. virtual RTCError SendData(int channel_id, const SendDataParams& params, const rtc::CopyOnWriteBuffer& buffer) = 0; // Closes |channel_id| gracefully. Returns an error if |channel_id| is not // open. Data sent after the closing procedure begins will not be // transmitted. The channel becomes closed after pending data is transmitted. virtual RTCError CloseChannel(int channel_id) = 0; // Sets a sink for data messages and channel state callbacks. Before media // transport is destroyed, the sink must be unregistered by setting it to // nullptr. virtual void SetDataSink(DataChannelSink* sink) = 0; // Returns whether this data channel transport is ready to send. // Note: the default implementation always returns false (as it assumes no one // has implemented the interface). This default implementation is temporary. virtual bool IsReadyToSend() const = 0; }; OpenChannel && CloseChannel:由于上层的多个DataChannel是共享同一个底层传输通道的,因此,需要将上层DataChannel以其标识值sid作为channel_id注册进入底层传输通道中;SendData:提供了数据向网络层传送的接口;SetDataSink: 如前文所述,以回调的形式提供了数据/状态上报;IsReadyToSend:提供状态查询接口。

现在,我们回答引言中的第二个问题:SCTP底层传输对象DataChannelTransportInterface到底实体类是哪个?什么时候创建的?

3.1 DataChannelTransportInterface创建时机追踪

搜索整个WebRTC工程,可以看到有四个类实现了DataChannelTransportInterface,那么,什么时候,什么条件下创建哪个实体类? 在这里插入图片描述 采用倒推法:首先,DataChannelController提供了DataChannelTransportInterface的setter方法set_data_channel_transport,并且DataChannelController是PC的一个成员,因此,很可能是PC的一个方法中调用的。去PC的实现文件中查看,果然如此:就在PC的SetupDataChannelTransport_n中

3.1.1 PeerConnection::SetupDataChannelTransport_n bool PeerConnection::SetupDataChannelTransport_n(const std::string& mid) { // 1. 根据DataChannel的mid值获取底层的Transport DataChannelTransportInterface* transport = transport_controller_->GetDataChannelTransport(mid); if (!transport) { RTC_LOG(LS_ERROR) case cricket::DCT_SCTP: case cricket::DCT_DATA_CHANNEL_TRANSPORT_SCTP: case cricket::DCT_DATA_CHANNEL_TRANSPORT: // 1. 在网络线程中执行PeerConnection::SetupDataChannelTransport_n方法 // 该方法使得底层Transport与DataChannelController关联上。 if (!network_thread()->Invoke( RTC_FROM_HERE, rtc::Bind(&PeerConnection::SetupDataChannelTransport_n, this, mid))) { return false; } // 2. 调用sctp_data_channels_中所有的DataChannel的OnTransportChannelCreated // 方法,该方法使得DataChannel与DataChannelController关联上,同时将通道sid值 // 注册到底层Transport中。 // All non-RTP data channels must initialize |sctp_data_channels_|. for (const auto& channel : *data_channel_controller_.sctp_data_channels()) { channel->OnTransportChannelCreated(); } return true; case cricket::DCT_RTP: default: // 此处省略了RTP通道处理代码 return true; } return false; }

划重点: CreateDataChannel通过调用SetupDataChannelTransport_n方法,将底层Transport与DataChannelController关联上;再挨个调用DataChannel::OnTransportChannelCreated,将DataChannel与DataChannelController关联上,并将表征DataChannel的sid值注册到底层的Transport中。至此,Transport——DataChannelController——DataChannel的任督二脉被打通了。

继续跟踪CreateDataChannel的调用,发现其在两个方法中被调用,一个是PeerConnection::UpdateDataChannel;一个是PeerConnection::CreateChannels。

搜索CreateChannels方法调用,发现该方法在PeerConnection::ApplyLocalDescription && PeerConnection::ApplyRemoteDescription中被调用,但是位置都在这样一个分支中,如下图源码所示:表明CreateChannels只有在Plan B这种SDP格式中才会被调用,而该方式将会被淘汰,因此,我们就不再关注该路径了。后续只考虑UpdateDataChannel这条路径。 在这里插入图片描述

3.1.3 PeerConnection::UpdateDataChannel RTCError PeerConnection::UpdateDataChannel( cricket::ContentSource source, const cricket::ContentInfo& content, const cricket::ContentGroup* bundle_group) { // 1. 如果数据通道类别为DCT_NONE,表示不允许创建DataChanel的,说明DataChannel是不存在的 // 直接返回OK即可 if (data_channel_type() == cricket::DCT_NONE) { // If data channels are disabled, ignore this media section. CreateAnswer // will take care of rejecting it. return RTCError::OK(); } // 2. 如果数据通道对应的mLine是被拒绝了,则进入销毁程序。 // Transport——DataChannelController——DataChannel关联线路要断开连接, // 并销毁已创建的底层数据通道以及DataChannel if (content.rejected) { RTC_LOG(LS_INFO) RTC_LOG(LS_INFO) const MediaContentDescription* data_desc = contendia_description(); if (data_desc && cricket::IsRtpProtocol(data_desc->protocol())) { data_channel_controller_.UpdateRemoteRtpDataChannels( GetActiveStreams(data_desc)); } } } return RTCError::OK(); }

划重点: UpdateDataChannel方法通过入参ContentInfo提供的信息来更新数据通道,如果content.rejected为真,则说明对方或者己方拒绝使用数据通道,那么进入数据通道的销毁程序DestroyDataChannelTransport;如果content.rejected为假,说明目前数据通道是被认可接受的,那么进入数据通道的创建程序CreateDataChannel。我们要知道ContentInfo结构对应于SDP中的一个m Section(即mLine),其name属性表示的是该Section的mid(也许后续该属性名会被改为mid),详细信息见文章 WebRTC源码分析——ContentInfo && ContentInfo && ContentSource

继续跟踪UpdateDataChannel的调用,只在PeerConnection::UpdateTransceiversAndDataChannels中被调用。那么继续跟踪UpdateTransceiversAndDataChannels方法。

3.1.4 PeerConnection::UpdateTransceiversAndDataChannels RTCError PeerConnection::UpdateTransceiversAndDataChannels( cricket::ContentSource source, const SessionDescriptionInterface& new_session, const SessionDescriptionInterface* old_local_description, const SessionDescriptionInterface* old_remote_description) { // 1. 只有Unified Plan才能调用该方法 RTC_DCHECK(IsUnifiedPlan()); // 2. 获取新sdp结构中的ContentGroup const cricket::ContentGroup* bundle_group = nullptr; if (new_session.GetType() == SdpType::kOffer) { auto bundle_group_or_error = GetEarlyBundleGroup(*new_session.description()); if (!bundle_group_or_error.ok()) { return bundle_group_or_error.MoveError(); } bundle_group = bundle_group_or_error.MoveValue(); } // 3. 遍历新SDP结构体中的所有ContentInfo,依据新ContentInfo与旧ContentInfo来 // 更新Transceivers(存储了VideoTrack、AudioTrack)与底层传输通道的关系, // 更新DataChannel与底层传输通道的关系。 const ContentInfos& new_contents = new_session.description()->contents(); for (size_t i = 0; i ...//省略了video audio的处理,因为内容过长,且与当前讨论内容无关 // 3.2 应用数据 } else if (media_type == cricket::MEDIA_TYPE_DATA) { // 3.2.1 GetDataMid()返回的是sctp_mid_值,若sctp_mid_存在,且与ContentInfo中 // 的mid不相同,则忽略。 这样处理,造成只有第一个应用数据类型的ContentInfo // (mLine)才会往后处理,进行通道数据更新。因此,只有第一个ContentInfo是有效的 if (GetDataMid() && new_content.name != *GetDataMid()) { // Ignore all but the first data section. RTC_LOG(LS_INFO) LOG_AND_RETURN_ERROR(RTCErrorType::INTERNAL_ERROR, "Unknown section type."); } } return RTCError::OK(); }

划重点: UpdateTransceiversAndDataChannels方法在处理数据通道时,就是遍历了新的SDP中所有数据类型的ContentInfo结构,确保只有第一个数据结构的ContentInfo得到有效的处理,其他数据ContentInfo将都被忽略。

继续跟踪UpdateTransceiversAndDataChannels方法调用,发现该方法会在PeerConnection::ApplyRemoteDescription && PeerConnection::ApplyLocalDescription中被调用,而这两个方法处理过程基本是一致的。因此,只对ApplyRemoteDescription做一个基础介绍。

3.1.5 PeerConnection::ApplyRemoteDescription RTCError PeerConnection::ApplyLocalDescription( std::unique_ptr desc) { // 1. 检查 // 1.1 信令线程执行该方法 RTC_DCHECK_RUN_ON(signaling_thread()); // 1.2 入参不可为空 RTC_DCHECK(desc); // 2. 更新统计,以便获取最新的统计信息,有些流可能因更新会话导致被移除了 // Update stats here so that we have the most recent stats for tracks and // streams that might be removed by updating the session description. stats_->UpdateStats(kStatsOutputLevelStandard); // 3. 使用old_local_description保存旧的本地SDP结构,以便与新的本地SDP结构进行对比。 // 当设置新的本地SDP时,需要将拿到将要被替换的SDP结构的拥有权(通过move语义), // 要被替换的SDP结构可能会与old_local_description是同一个SDP结构,使得该 // SDP在本方法内一直是有效的,不会被销毁。 // Take a reference to the old local description since it's used below to // compare against the new local description. When setting the new local // description, grab ownership of the replaced session description in case it // is the same as |old_local_description|, to keep it alive for the duration // of the method. // 3.1 使用临时变量old_local_description保存当前本地SDP结构 // 也即pending_local_description_ && current_local_description_中的一个 // 优先是pending_local_description_,why? const SessionDescriptionInterface* old_local_description = local_description(); // 3.2 replaced_local_description用来获取将被替换的SDP结构的拥有权 std::unique_ptr replaced_local_description; // 3.3 根据新本地sdp类别做不同处理 SdpType type = desc->GetType(); // 3.3.1 若本地sdp类别是kAnswer,意味着本端协商已经结束。 if (type == SdpType::kAnswer) { // 保存被替换的SDP结构 replaced_local_description = pending_local_description_ ? std::move(pending_local_description_) : std::move(current_local_description_); // 当前本地SDP设置为新SDP current_local_description_ = std::move(desc); // 当前本地pending sdp设置为空即可,因为已经是协商结束了,没必要pending了 pending_local_description_ = nullptr; // 当前远端SDP设置为远端pending sdp,同样是因为已经是协商结束了,需要将pending // sdp应用上了 current_remote_description_ = std::move(pending_remote_description_); // 3.3.2 若本地sdp类别是其他,意味着本端协商还未完毕。 } else { // 由于协商还未完成,因此,被替换的只可能是本地pending sdp replaced_local_description = std::move(pending_local_description_); // 由于协商还未完成,pending当前新的本地sdp即可 pending_local_description_ = std::move(desc); } // 3.4 此时,新的sdp要么是存储在pending_local_description_, /// 要么是current_local_description_ // The session description to apply now must be accessed by // |local_description()|. RTC_DCHECK(local_description()); // 4. 报告联播simulcast信息统计 // Report statistics about any use of simulcast. ReportSimulcastApiVersion(kSimulcastVersionApplyLocalDescription, *local_description()->description()); // 5. 确定本端是呼叫方Caller,还是被叫方Callee if (!is_caller_) { // 如果远端SDP存在,说明远端SDP先被设置,因此,本端是被叫方Calle if (remote_description()) { // Remote description was applied first, so this PC is the callee. is_caller_ = false; // 近端SDP先被设置,因此,本端是呼叫方Caller } else { // Local description is applied first, so this PC is the caller. is_caller_ = true; } } // 6. 应用SDP到底层传输,根据mid以及相关描述信息创建底层传输Transport RTCError error = PushdownTransportDescription(cricket::CS_LOCAL, type); if (!error.ok()) { return error; } // 7. Unifiled plan // 更新上层Transceiver && DataChannel if (IsUnifiedPlan()) { RTCError error = UpdateTransceiversAndDataChannels( cricket::CS_LOCAL, *local_description(), old_local_description, remote_description()); if (!error.ok()) { return error; } .... } .... return RTCError::OK(); }

划重点: ApplyLocalDescription方法在步骤6,PushdownTransportDescription方法将新的sdp应用到底层——Transport层,用来创建对应于上层结构的传输层对象,这是我们之前一直苦苦追寻的结果,即:我们会在何时创建DataChannel对应的底层传输DataChannelTransportInterface接口的实体对象。另外在步骤7,会调用UpdateTransceiversAndDataChannels()来更新数据通道,建立起上下几层对象的连接,正如前文所述。

如果顺着ApplyLocalDescription的调用关系往上翻,我们可以知道在建立会话主流程的SetLocalDesciption方法中是最终触发这一系列动作的时机。SetLocalDesciption——>DoSetLocalDesciption——>ApplyLocalDescription。同理,设置远端会话同样做了类似的事。由于后续会专门写文章去阐述SetLocalDesciption的细节,因此,此处就此打住不再向上追溯。我们把重点放在PushdownTransportDescription方法上,一路向下追溯,看看底层Transport创建过程,探个究竟。

3.1.6 PeerConnection::PushdownTransportDescription RTCError PeerConnection::PushdownTransportDescription( cricket::ContentSource source, SdpType type) { RTC_DCHECK_RUN_ON(signaling_thread()); if (source == cricket::CS_LOCAL) { const SessionDescriptionInterface* sdesc = local_description(); RTC_DCHECK(sdesc); return transport_controller_->SetLocalDescription(type, sdesc->description()); } else { const SessionDescriptionInterface* sdesc = remote_description(); RTC_DCHECK(sdesc); return transport_controller_->SetRemoteDescription(type, sdesc->description()); } }

由源码可知,根据SDP是远端还是近端的,按条件调用JsepTransportController的SetLocalDescription或者是SetRemoteDescription,由于二者几乎工作性质相同,只需先分析SetLocalDescription即可。

3.1.6 JsepTransportController::SetLocalDescription

在这里插入图片描述 上图是该方法的描述,是JsepTransportController的主要方法。将SDP应用到底层传输层,按需创建/销毁传输层对象,更新他们的属性。源码如下:

RTCError JsepTransportController::SetLocalDescription( SdpType type, const cricket::SessionDescription* description) { // 1. 传输层的方法都工作在network线程,包括SetLocalDescription也需要 // 在网络线程中执行 if (!network_thread_->IsCurrent()) { return network_thread_->Invoke( RTC_FROM_HERE, [=] { return SetLocalDescription(type, description); }); } // 2. 设置ICE控制角色,本端是协商的主导者,还是对端是主导者 if (!initial_offerer_.has_value()) { initial_offerer_.emplace(type == SdpType::kOffer); if (*initial_offerer_) { SetIceRole_n(cricket::ICEROLE_CONTROLLING); } else { SetIceRole_n(cricket::ICEROLE_CONTROLLED); } } // 3. 调用ApplyDescription_n return ApplyDescription_n(/*local=*/true, type, description); } 3.1.7 JsepTransportController::ApplyDescription_n RTCError JsepTransportController::ApplyDescription_n( bool local, SdpType type, const cricket::SessionDescription* description) { ... // 遍历sdp中所有ContentInfo for (const cricket::ContentInfo& content_info : description->contents()) { // 被拒绝的content是无效的,因此,我们不应该为其创建Transport; // 如果Content属于一个bundle,却又不是该bundle的第一个content,那么我们应该也要 // 忽略该content,因为属于一个bundle的content共享一个Transport进行传输,在遍历 // 该bundle的第一个content时会去创建这个共享的Transport。 // Don't create transports for rejected m-lines and bundled m-lines." if (content_info.rejected || (IsBundled(content_info.name) && content_info.name != *bundled_mid())) { continue; } // 创建JsepTransport error = MaybeCreateJsepTransport(local, content_info, *description); if (!error.ok()) { return error; } } ... return RTCError::OK(); } bool IsBundled(const std::string& mid) const { return bundle_group_ && bundle_group_->HasContentName(mid); } absl::optional bundled_mid() const { absl::optional bundled_mid; if (bundle_group_ && bundle_group_->FirstContentName()) { bundled_mid = *(bundle_group_->FirstContentName()); } return bundled_mid; }

我删除了与Transport不相关的代码,留待以后分析。只保留重要的代码段如上源码所示。ApplyDescription_n方法中会遍历SDP中所有的ContentInfo,也即m Section的表征。根据源码上所述的方式来调用 MaybeCreateJsepTransport() 方法来创建JsepTransport。

由JsepTransportController.bundle_group_ 成员可知,实际应用中,SDP中一般只有一个bundle group。绝大多数情况下都是采用bundle形式进行传输,此时,这样可以减少底层Transport的数量,因此,也能减少需要分配的端口数。如下SDP的示例,4个mline都属于一个group,这个group的名字为默认的BUNDLE,其中0 1 2 3为4个mid值。这4个m section所代表的媒体将采用同一个JsepTransport 在这里插入图片描述

3.1.8 JsepTransportController::MaybeCreateJsepTransport RTCError JsepTransportController::MaybeCreateJsepTransport( bool local, const cricket::ContentInfo& content_info, const cricket::SessionDescription& description) { // 1. 必须网络线程中执行 RTC_DCHECK(network_thread_->IsCurrent()); // 2. 如果对应的JsepTransport已经存在则返回就好了 cricket::JsepTransport* transport = GetJsepTransportByName(content_info.name); if (transport) { return RTCError::OK(); } // 3. 判断Content中的媒体描述中是否存在加密参数,这些参数是给SDES使用的 // 而JsepTransportController.certificate_是给DTLS-SRTP使用的 // 二者不可同时存在,因此,需要做下判断。 const cricket::MediaContentDescription* content_desc = content_info.media_description(); if (certificate_ && !content_desc->cryptos().empty()) { return RTCError(RTCErrorType::INVALID_PARAMETER, "SDES and DTLS-SRTP cannot be enabled at the same time."); } // 4. 创建ice层的传输对象—>负责管理Candidates,联通性检测,收发数据包 // 注意,使用的是共享智能指针保存的。 rtc::scoped_refptr ice = CreateIceTransport(content_info.name, /*rtcp=*/false); RTC_DCHECK(ice); // 5. 如果外界配置了使用DatagramTransport来传输(实质上采用QUIC协议) // 则会创建DatagramTransport。 // 注意DatagramTransport层内置了ice层的传输对象,也就是说ice层的传输对象 // 是其底层对象 std::unique_ptr datagram_transport = MaybeCreateDatagramTransport(content_info, description, local); if (datagram_transport) { datagram_transport->Connect(ice->internal()); } // 6. 创建dtls层的传输对象——>提供Dtls握手逻辑,密钥交换。 // 注意其dtls内置了ice层的传输对象,其层次与DatagramTransport是平行关系 std::unique_ptr rtp_dtls_transport = CreateDtlsTransport(content_info, ice->internal(), nullptr); std::unique_ptr rtcp_dtls_transport; std::unique_ptr unencrypted_rtp_transport; std::unique_ptr sdes_transport; std::unique_ptr dtls_srtp_transport; std::unique_ptr datagram_rtp_transport; // 7. 如果RTCP与RTP不复用,并且媒体是使用RTP协议传输的,则需要创建属于传输RTCP的 // ice层的传输对象,以及dtls层的传输对象 rtc::scoped_refptr rtcp_ice; if (config_.rtcp_mux_policy != PeerConnectionInterface::kRtcpMuxPolicyRequire && content_info.type == cricket::MediaProtocolType::kRtp) { RTC_DCHECK(datagram_transport == nullptr); rtcp_ice = CreateIceTransport(content_info.name, /*rtcp=*/true); rtcp_dtls_transport = CreateDtlsTransport(content_info, rtcp_ice->internal(), /*datagram_transport=*/nullptr); } // 8. 如果允许采用quic协议 // 创建rtp层传输对象——>DatagramRtpTransport // 该对象使用dtls同层次的DatagramTransport来传输(基于quic协议) // Only create a datagram RTP transport if the datagram transport should be // used for RTP. if (datagram_transport && config_.use_datagram_transport) { // TODO(sukhanov): We use unencrypted RTP transport over DatagramTransport, // because MediaTransport encrypts. In the future we may want to // implement our own version of RtpTransport over MediaTransport, because // it will give us more control over things like: // - Fusing // - Rtp header compression // - Handling Rtcp feedback. RTC_LOG(LS_INFO) internal(), datagram_transport.get()); } // 9. 根据是否使用加密以及加密手段,来创建RTP层不同的传输对象 // 9.1 不使用加密,则创建不使用加密手段的rtp层传输对象——>RtpTransport // 注意:仍然传递了dtls层的传输对象,但该对象可以不进行加密,直接将上层的 // 包传递给ice层传输对象。 if (config_.disable_encryption) { RTC_LOG(LS_INFO) cryptos().empty()) { sdes_transport = CreateSdesTransport( content_info.name, rtp_dtls_transport.get(), rtcp_dtls_transport.get()); RTC_LOG(LS_INFO) sctp_transport = config_.sctp_factory->CreateSctpTransport(rtp_dtls_transport.get()); } // 11. 如果使用datagram_transport来传输应用数据 // 则data_channel_transport设置为其子类对象datagram_transport DataChannelTransportInterface* data_channel_transport = nullptr; if (config_.use_datagram_transport_for_data_channels) { data_channel_transport = datagram_transport.get(); } // 12. 创建JsepTransport对象,来容纳之前创建的各层对象 // ice层:两个传输对象ice、rtcp_ice; // dtls层/datagram层:rtp_dtls_transport、rtcp_dtls_transport/datagram_transport // 基于dtls层的rtp层:unencrypted_rtp_transport、sdes_transport、dtls_srtp_transport // 基于datagram层的rtp层:datagram_rtp_transport // 基于dtls层的应用数据传输层:sctp_transport // 基于datagram层的应用数据传输层:data_channel_transport std::unique_ptr jsep_transport = std::make_unique( content_info.name, certificate_, std::move(ice), std::move(rtcp_ice), std::move(unencrypted_rtp_transport), std::move(sdes_transport), std::move(dtls_srtp_transport), std::move(datagram_rtp_transport), std::move(rtp_dtls_transport), std::move(rtcp_dtls_transport), std::move(sctp_transport), std::move(datagram_transport), data_channel_transport); // 13. 绑定JsepTransport信号-JsepTransportController槽 jsep_transport->rtp_transport()->SignalRtcpPacketReceived.connect( this, &JsepTransportController::OnRtcpPacketReceived_n); jsep_transport->SignalRtcpMuxActive.connect( this, &JsepTransportController::UpdateAggregateStates_n); jsep_transport->SignalDataChannelTransportNegotiated.connect( this, &JsepTransportController::OnDataChannelTransportNegotiated_n); // 14. 将创建的JsepTransport添加到JsepTransportController的成员上 // 14.1 添加到mid_to_transport_ SetTransportForMid(content_info.name, jsep_transport.get()); // 14.2 添加到jsep_transports_by_name_ jsep_transports_by_name_[content_info.name] = std::move(jsep_transport); // 15. 更新状态,进行ICE UpdateAggregateStates_n(); return RTCError::OK(); }

通过对MaybeCreateJsepTransport方法的详尽分析,我们可以知道底层创建了三类用于传输应用数据的底层传输对象:

SctpTransportInternal:基于SCTP/DTLS的传输;DatagramTransportInterface:基于QUIC协议的传输;DtlsSrtpTransport:基于RTP/DTLS的传输;

到此,我们已经知道创建数据传输的底层传输对象的时机了,但是确切会使用哪个,并且最终实现DataChannelTransportInterface类是哪个?还无法明确。因为:

只有DatagramTransportInterface这个接口是继承DataChannelTransportInterface接口的,实体类由外部提供的MediaTransportFactory来产生。SctpTransportInternal并非是继承DataChannelTransportInterface接口的实体类,因此,提供给DataChannel直接使用的肯定不是该类。

那么,我们何去何从呢?我看看JsepTransport这个方法的构造 以及 之前出现过的获取DataChannelTransportInterface的data_channel_transport方法。

3.1.8 JsepTransport::JsepTransport JsepTransport::JsepTransport(...) : ... // 1. 创建SctpDataChannelTransport对象,利用SctpTransportInternal对象来实现 // DataChannelTransportInterface接口功能。 sctp_data_channel_transport_( sctp_transport ? std::make_unique( sctp_transport.get()) : nullptr), ...) { // 2. 参数检查 // 2.1 基础层——ice层和dtls层不可缺少 RTC_DCHECK(ice_transport_); RTC_DCHECK(rtp_dtls_transport_); // 2.2 基础层——提供给rtcp的ice层和dtls层必须同时存在或者不存在 // |rtcp_ice_transport_| must be present iff |rtcp_dtls_transport_| is // present. RTC_DCHECK_EQ((rtcp_ice_transport_ != nullptr), (rtcp_dtls_transport_ != nullptr)); // 2.3 rtp层根据加密手段,三者只能存一 // Verify the "only one out of these three can be set" invariant. if (unencrypted_rtp_transport_) { RTC_DCHECK(!sdes_transport); RTC_DCHECK(!dtls_srtp_transport); } else if (sdes_transport_) { RTC_DCHECK(!unencrypted_rtp_transport); RTC_DCHECK(!dtls_srtp_transport); } else { RTC_DCHECK(dtls_srtp_transport_); RTC_DCHECK(!unencrypted_rtp_transport); RTC_DCHECK(!sdes_transport); } // 3. 给sctp层传输配上dtls层传输对象 if (sctp_transport_) { sctp_transport_->SetDtlsTransport(rtp_dtls_transport_); } // 4. 如果提供了基于Datagram的rtp层传输,并且也有默认的rtp传输(即上面三者之一) // 那么先创建组合了二者的CompositeRtpTransport,相当于是双核。 // if (datagram_rtp_transport_ && default_rtp_transport()) { composite_rtp_transport_ = std::make_unique( std::vector{ datagram_rtp_transport_.get(), default_rtp_transport()}); } // 5. 如果提供了 if (data_channel_transport_ && sctp_data_channel_transport_) { composite_data_channel_transport_ = std::make_unique( std::vector{ data_channel_transport_, sctp_data_channel_transport_.get()}); } }

划重点:

构造函数省略直接给成员赋值的部分,只留有一个:使用SctpTransportInternal来构建SctpDataChannelTransport类别的成员sctp_data_channel_transport_,SctpDataChannelTransport是实现了DataChannelTransportInterface接口实体类。如果提供了基于Datagram的rtp层传输,并且也有默认的rtp传输(即非加密,sdes加密,dtls加密三者之一), 那么先创建组合了二者的CompositeRtpTransport,相当于是双核。如果提供了基于Datagram的sctp层传输,并且默认的sctp传输方式也存在,那么先创建组合了二者的CompositeDataChannelTransport,双核。

PS1: CompositeRtpTransport的说明很重要 在这里插入图片描述 PS2: CompositeDataChannelTransport的说明很重要在这里插入图片描述 当JsepTransport向外提供数据传输通道时,可以看看是如何做的——data_channel_transport方法

3.1.9 JsepTransport::data_channel_transport webrtc::DataChannelTransportInterface* data_channel_transport() const { rtc::CritScope scope(&accessor_lock_); if (composite_data_channel_transport_) { return composite_data_channel_transport_.get(); } else if (sctp_data_channel_transport_) { return sctp_data_channel_transport_.get(); } return data_channel_transport_; }

顺序是:

选用组合类型的传输CompositeDataChannelTransport存在时,选用组合类型(有可能在协商结束后,确定了使用哪种传输,将CompositeDataChannelTransport删除的情况)当sctp-dtls存在时,使用SctpDataChannelTransport。当sctp-dtls不存在,基于datagram的,由外部提供的MediaTransportFactory创建的,实现了DatagramTransportInterface接口的实体类(一般而言是实现了quic协议的传输)

到此,我们确认了最终提供给DataChanel使用的,实现了DataChannelTransportInterface接口的底层传输类(CompositeDataChannelTransport、SctpDataChannelTransport、由MediaTransportFactory创建的实现了DatagramTransportInterface接口的实体类),何时创建的它们(在PC调用SetLocalDescription && SetRemoteDescription时)。

3.2 作图以做总结

在这里插入图片描述

绿色为两个起点和一个终点:启动Transport创建的两个顶层API,给DataChanel设置Transport的终点站。红色的部分为创建DataChannel可使用的Transport实体类的地方;蓝色部分主要体现在方法JsepTransportController::MaybeCreateJsepTransport中,该方法创建了WebRTC中Transport的所有类别,每个蓝色和红色部分都在创建不同层次的一个Transport。 4. DataChannel

DataChannelController与DataChannel的关系如下图所示: 在这里插入图片描述

4.1 DataChannel的成员变量 控制数据上下行成员: DataChannelObserver* observer_ DataChannelProviderInterface* provider_通道属性成员: std::string label_; InternalDataChannelInit config_; cricket::DataChannelType data_channel_type_;通道状态相关成员: DataState state_; HandshakeState handshake_state_; bool connected_to_provider_; bool send_ssrc_set_; bool receive_ssrc_set_; uint32_t send_ssrc_; uint32_t receive_ssrc_ bool writable_; bool started_closing_procedure_ = false;数据发送接收统计 && 存储 uint32_t messages_sent_; uint64_t bytes_sent_; uint32_t messages_received_; uint64_t bytes_received_; uint64_t buffered_amount_; PacketQueue queued_control_data_; PacketQueue queued_received_data_; PacketQueue queued_send_data_;状态信号 sigslot::signal1 SignalOpened; sigslot::signal1 SignalClosed;

后续以成员变量为线索,来分析DataChannel的功能,前文实际上已对 “控制数据上下行成员” 和 “通道属性成员”的作用作过描述,因此,后续将不再赘述。

4.2 DataChannel的状态流转

DataChannel在该层维护了数据通道的状态:DataState state_;

enum DataState { kConnecting, kOpen, // The DataChannel is ready to send data. kClosing, kClosed }; kConnecting:当DataChannel被创建出来时,就是kConnecting状态。此刻,DataChannel还不能发送数据。数据通道在发送应用层数据前(也即进入kOpen状态前),还需要经历握手阶段,涉及到另外一个成员HandshakeState handshake_state_。 如果握手协商是带外协商,那么handshake_state_默为kHandshakeReady,也即握手已完成。此时,不论是DataChannel的主动打开方,还是被动打开方,只要底层DataChannelTransport创建成功,并且处于数据可写状态,那么DataChannel会被更新为kOpen状态,可以发送应用层数据;若采用带内协商,刚创建完的DataChannel视其为主动创建方,还是被动打开方握手状态分别为 kHandshakeShouldSendOpen or kHandshakeShouldSendAck。主动打开方在底层DataChannelTransport创建成功,并且处于数据可写状态时,DataChannel会向对方发送Open Message控制命令,并更新自己的握手状态为kHandshakeWaitingForAck。而对方,DataChannel还未创建,只是在收到Open Message时才会临时创建出DataChannel对象,此时其就处于kConnecting状态,握手状态为kHandshakeShouldSendAck,由于底层DataChannelTransport已经打通(不然Open Message咋收到的呢~~),因此,会直接回复Ack Message,并更新自己的握手状态为kHandshakeReady,连接状态更新为kOpen。而主动打开方也会在收到Ack Message后更新自己的握手状态为kHandshakeReady,连接状态更新为kOpen。 enum HandshakeState { kHandshakeInit, kHandshakeShouldSendOpen, kHandshakeShouldSendAck, kHandshakeWaitingForAck, kHandshakeReady }; kOpen:该状态是DataChannel能正常发送接收数据的状态,数据发送接收见4.3分析kClosing:有三种情况可以由kOpen状态进入kClosing状态,这是关闭中的状态。一种是本端主动调用Close方法关闭DataChannel,设置kClosing状态后,由于此时底层的Transport还是正常工作的,因此,可以等待正在排队的应用层数据都发送后,再真正关闭通道进入kClosed状态;第二种是对端主动调用Close方法,本端收到底层Transport的进入开始关闭流程的信号,DataChannel在槽函数OnClosingProcedureStartedRemotely()中进行相应处理,此时会直接清除正在排队的应用层数据和控制数据(底层都要关闭了,不允许继续发送数据了),设置状态为kClosing;最后一种是异常情况下的紧急处理调用CloseAbruptly()来关闭通道,这些紧急情况有:发送数据时,发送数据失败,原因并非是底层Transport处于阻塞状态 or 排队的数据超出了最大排队限度(因为底层阻塞没有及时发送出去);接收数据时,排队的数据超出了最大排队限度(因为上层业务阻塞没有及时取走数据);还有就是协商时mline被对方拒绝 or DTLS通道关闭,底层Transport抛出信号,DataChannel在槽OnTransportChannelClosed()中调用CloseAbruptly()来响应这种情况。kClosed:代表关闭状态,进入kClosing状态的DataChannel跟着会进入kClosed状态,不过由于进入kClosing状态的路径不一样,那么进入kClosed状态的过程也就不太一样。一种是本端主动调用Close方法关闭DataChannel,设置kClosing状态后调用UpdateState()。若是无排队的数据要发送,那么直接调用DataChannelController的RemoveSctpDataStream,这样会触发底层Transport关闭通道,并已信号形式告知DataChannel,DataChannel在槽函数OnClosingProcedureComplete响应该信号,并将状态设置为kClosed。若是有数据发送呢?需要等待底层通道告知数据可写,触发DataChannel的槽OnChannelReady,在该方法中阻塞的将排队数据都发送出去,然后调用UpdateState(),这样就进入了前面的状况。第二种是对端主动调用Close方法时,本端依次会收到底层Transport的进入开始关闭流程的信号、关闭流程已完成的信号,DataChannel依次在槽函数OnClosingProcedureStartedRemotely、OnClosingProcedureComplete中将状态设置为kClosing、kClosed。最后一种是异常情况下的紧急处理调用CloseAbruptly()来关闭通道,之前讨论过这些异常情况,该方法中会清理DataChannel,并依次将状态设置为kClosing、kClosed。 4.3 DataChannel数据发送、接收、缓存 、统计

数据的发送分两个触发点:一个是用户主动调用DataChannel::Send方法发送数据;一个是底层Transport由Block阻塞状态进入可发送状态,DataChannelTransport以信号方式通知DataChannel数据可发送,DataChannel响应该信号进行数据发送。

4.3.1 用户主动发送数据

当用户层通过调用DataChannel::Send方法发送数据,调用流程如下图所示: 在这里插入图片描述 需要重点提出来的几点列举如下:

先判断DataChannel层的发送队列是否为空,不为空意味着底层Transport处于block状态,即便继续调用底层发送函数也无用,最终还是得排队,因此,还不如就此排队返回。调用底层Transport进行发送时,失败的原因大致分两种,一种是底层Transport 阻塞住了,这种状态是不会主动告知DataChannel,只有当发送数据时,从返回值得知,这种状态是可恢复到正常可写状态的,一旦底层数据正常发送出去,不繁忙时即可通告DataChannel;一种是其他原因,比如通道已经被关闭等其他原因,这种是不可恢复的,因此,直接调用CloseAbruptly()去关闭,清理上层通道。当上层排队数据超过16k,也会触发CloseAbruptly()去关闭,清理上层通道。 4.3.2 底层Transport由block变为可写——>发送排队数据

DataChannel可能发送的数据一方面是应用层用户要发送的数据;另一方面是前文所述的,在进行带内协商的情况下,kConnecting状态时发送握手控制信令。因此,需要排队的数据有这两类,DataChannel用了两个数据队列成员 和 两组数据排队、发送方法。这两组成员和方法作用是对称类似的。

当底层Transport由阻塞状态变为可写状态时,将以信号-槽的形式通知DataChannel层进行处理。处理的方式就是调用对应的方法,将已排队的数据发送出去。需要注意的一点是:图中省略排队的控制信令的处理,并且控制信令往往需要先发送。也即,在调用DataChannel::SendQueuedDataMessages发送用户数据之前,需要调用DataChannel::SendQueuedControlMessages发送排队的控制信令。二者处理流程是一模一样的,因此图中省略该部分。

4.3.3 接收数据

当收到底层Transport传递上来的数据后,处理流程如下图所示: 在这里插入图片描述 需要再次强调的是:

用于握手的控制信令数据 和 用户应用数据从同一处传递上来,通过数据的类别字段进行区分不要期待能收到Open这个信令,作为被动打开方,收到Open信令时,DataChannel才会被DataChannelController创建,因此,DataChannel层是不会收到Open信令的。只有主动打开方的DataChannel会收到Ack信令。接收用户会有两个出处:一个是当DataChannel已经处于kOpen状态,并且也存在通道的观察者DataChannelObserver,那么数据会立马递送给DataChannelObserver进行用户层处理;否则,数据会进入接收队列。接收队列中的数据何时会被递送到DataChannelObserver?一种情况是当DataChannel注册Observer时,会执行一次DeliverQueuedReceivedData()方法,将可能的已接收的数据一次性递送上去;一种情况是当DataChannel更新其状态到kOpen时,也会执行一次DeliverQueuedReceivedData()方法,将可能的已接收的数据一次性递送上去。 4.3.3 数据的缓存

如上所述,DataChannel使用了三个队列,分别缓存发送的用户层数据,发送的握手控制信令,接收的用户层数据 。这三个队列使用的类如下所示,封装了标准模板库中的双端队列。

class PacketQueue final { public: size_t byte_count() const { return byte_count_; } bool Empty() const; std::unique_ptr PopFront(); void PushFront(std::unique_ptr packet); void PushBack(std::unique_ptr packet); void Clear(); void Swap(PacketQueue* other); private: std::deque packets_; size_t byte_count_ = 0; }; void DataChannel::PacketQueue::PushFront(std::unique_ptr packet) { byte_count_ += packet->size(); packets_.push_front(std::move(packet)); }

值得注意得是:队列中存储的是DataBuffer的只能指针,也即存储进入PacketQueue中的包,实际上被PacketQueue所拥有,正如上述源码给出的PushFront方法实现,需要使用std::move来实现移动语义,转移所有权。

另外,DataBuffer实际由"写时复制"的buffer类CopyOnWriteBuffer构成,如下源码所示,具体分析将另写一篇文章来分析:WebRTC源码分析——写时复制缓存CopyOnWriteBuffer

struct DataBuffer { DataBuffer(const rtc::CopyOnWriteBuffer& data, bool binary) : data(data), binary(binary) {} // For convenience for unit tests. explicit DataBuffer(const std::string& text) : data(text.data(), text.length()), binary(false) {} size_t size() const { return data.size(); } rtc::CopyOnWriteBuffer data; // Indicates if the received data contains UTF-8 or binary data. // Note that the upper layers are left to verify the UTF-8 encoding. // TODO(jiayl): prefer to use an enum instead of a bool. bool binary; }; 5. 总结

行文至此,DataChannel及其相关的类DataChannelController && DataChannelTransport就介绍完了。三者的关系如下所示,后续捡要点总结下: 在这里插入图片描述

WebRTC的数据通道大致可分为用户层DataChannel、底层DataChannelTransport两层;DataChannelController作为夹在二者之间的连接层而存在。DataChannelController作为中间连接层,所起到的作用就是存储DataChannel、DataChannelTransport对象;建立DataChannel、DataChannelTransport对象之间的关联(信号-槽机制,以及直接的函数嵌套调用);做线程之间的转换(由于DataChannelTransport工作与network线程,而DataChannel工作与signal线程),确保DataChannel、DataChannelTransport的方法在正确的线程上执行。本文花了很大篇幅追踪了底层DataChannelTransport是何时被创建,创建的是哪个类别的对象,如何与DataChannel建立关联的,可以在详细捋一遍DataChannel层发送,接收数据都有排队逻辑,实现方式非常优雅高效,感兴趣的可以详细的进一步研究


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3